<?php

/**
 * This class extends a standard Zend_ACL for use with a database.
 * Written by Michael MistaGee Ziegler
 * License: LGPL v2 or above
 * The constructor expects a Zend_DB object as parameter.
 *
 *
 * Database structure:
 *
 * +------------------+       +----------------+       +-------------+      +--------------+
 * |    Resources     |       |    Access      |       |   Roles     |      |  Inheritance |
 * +------------------+       +----------------+       +-------------+      +--------------+
 * | *id              |<--.   | *role_id       |------>| *id         |<-----| *child_id    |
 * | parent_id        |---'`--| *resource_id  N|       +-------------+   `--| *parent_id   |
 * | *privilege      N|<------| *privilege    N|                            | order        |
 * +------------------+       | allow          |                            +--------------+
 *                            +----------------+
 *
 *
 *   *field = PRIMARY KEY( field )
 *   -----> = foreign key constraint
 *
 *   The actual table names should be: acl_resources, acl_access, acl_roles, acl_inheritance.
 *
 * access.allow is a boolean field, that specifies whether the respective rule is an allow rule or a deny rule (important for inherited access).
 *
 * The inheritance table stores which Role is to inherit rights from which parent rules. There can
 * be multiple parent rules. If a rule inherits rights from more than one parent, the first rule applicable
 * will be used to determine whether to allow or deny the access rights in question.
 * The order field stores in which order the parents are to be introduced to Zend_ACL, effectively setting
 * the order the parent rights are evaluated in.
 * Using a relational database for this is strongly advised, as it guarantees data integrity.
 *
 * If you intend to give each resource a specific name or collect other data about it, you should create
 * an extra table storing this data and put a foreign key referencing this into the resources table. Same
 * goes for the privileges.
 *
 */

/*
 require_once 'Zend/Db.php';

 require_once 'Zend/Acl.php';
 require_once 'Zend/Acl/Role.php';
 require_once 'Zend/Acl/Resource.php';
 */

class Sfs_Acl extends Zend_Acl {

    //	private $_cache;

    public function hasAllRolesOf( array &$searchRoles ){
        foreach( $searchRoles as $theRole )
        if( !$this->hasRole( $theRole ) )
        return false;
        return true;
    }

    public function __construct()
    {


        //$this->add(new Zend_Acl_Resource('default')); // 預設資源
        // 預設群組
        //$this->addRole(new Zend_Acl_Role('guest')); // 訪客
        //$this->allow(null, 'default');


        // I chose to write the field names into these SQL statements, so that the tables can actually contain more
        // fields than just the ones I need here without producing heavier DB load as neccessary.

        /// First: Create all the resources we have.

        // get Doctrine_Connection object
        $conn = Doctrine_Manager::getInstance()->connection();
        // execute SQL query, receive Doctrine_Connection_Statement
        $st = $conn->execute('SELECT DISTINCT resource_id, controller, parent_id FROM acl_resources_access_view');
        // fetch query result
        $resources = $st->fetchAll();

        $resCount = 0;
        $addCount  = 0;

        $allResources = array();
        foreach( $resources as $theRes ){
            if ($theRes['controller'] == null) {
                $allResources[] = $theRes['resource_id'];
            }
            else {

                $arr = explode(":",$theRes['controller']);
                $arr[] = 'index';
                foreach($arr as $val) {
                    $tt = $theRes['resource_id'].':'.$val;
                    if (!in_array($tt,$allResources))
                    $allResources[] = $tt;
                }

            }


        }

        //echo "<pre>";print_r($resources);
        $resCount = count($allResources);


        // 檢查父層資源是否存在
        foreach( $resources as $theRes ){

            if( $theRes['parent_id'] !== null && !in_array( $theRes['parent_id'], $allResources ) ){

                throw new Zend_Acl_Exception(
					"Resource id '".$theRes['parent_id']."' does not exist"
					);
            }
        }

        $addCount = 0;
        while( $resCount > $addCount ){

            foreach( $resources as $theRes ){
                // Check if parent resource (if any) exists
                // Only add if this resource hasn't yet been added and its parent is known, if any
                //if( !$this->has( $theRes['resource_id'])) echo $theRes['resource_id'].'--'. $theRes['parent_id'].' --<br>';
                $resource_arr = array();
                if ($theRes['controller'] === null) {
                    $resource_arr[] = $theRes['resource_id'];
                }
                else {
                    $arr = explode(":",$theRes['controller']);
                    //  index controller access
                    $arr[]= 'index';
                    foreach($arr as $val) {
                        $tt = $theRes['resource_id'].':'.$val;
                        if (!in_array($val,$resource_arr))
                        $resource_arr[]= $tt;
                    }

                }

                //                print_r($resource_arr);
                foreach($resource_arr as $val){
                    if( !$this->has( $val ) && ( $theRes['parent_id'] === null || $this->has( $theRes['parent_id'] ) )){
                        $this->add( new Zend_Acl_Resource( $val ), $theRes['parent_id'] );
                        //								echo "$addCount -- $val -- {$theRes['parent_id']} <br>";
                        $addCount++;
                    }
                }

            }
        }

        $query = "SELECT r.id, i.parent_id FROM acl_roles r LEFT JOIN acl_inheritance i
        ON r.id=i.child_id ORDER BY i.child_id, i.order_id";
        $roles = $conn->execute($query)->fetchAll();

        // Create an array that stores all roles and their parents
        $dbElements = array();
        foreach( $roles as $theRole ){
            if( !isset( $dbElements[ $theRole['id'] ] ) )
            $dbElements[ $theRole['id'] ] = array();
            if( $theRole['parent_id'] !== null )
            $dbElements[ $theRole['id'] ][] = $theRole['parent_id'];
        }

        // Now add to the ACL
        $dbElemCount  = count( $dbElements );
        $aclElemCount = 0;

        // while there are still elements left to be added
        while( $dbElemCount > $aclElemCount ){
            // Check every element in the db
            foreach( $dbElements as $theDbElem => $theDbElemParents ){
                // Check if a parent is invalid to prevent an infinite loop
                // if the relational DBase works, this shouldn't happen
                foreach( $theDbElemParents as $theParent ){
                    if( !array_key_exists( $theParent, $dbElements ) ){
                        require_once 'Zend/Acl/Exception.php';
                        throw new Zend_Acl_Exception(
							"Role id '$theParent' does not exist"
                        );
                    }
                }
                if( !$this->hasRole( $theDbElem ) &&            // if it has not yet been added to the ACL
                ( empty( $theDbElemParents )  ||            // and no parents exist or
                $this->hasAllRolesOf( $theDbElemParents ) // we know them all
                )
                ){
                    // we can add to ACL
                    $this->addRole( new Zend_Acl_Role( $theDbElem ), $theDbElemParents );
                    $aclElemCount++;
                }
            }
        }

        /// Now create all access rules
        $query = "SELECT role_id,resource_id,controller,privilege,allow,parent_id
        FROM acl_access_view ORDER BY allow " ;
        $access = $conn->execute($query) ->fetchAll();

        foreach( $access as $theRule ){
            if ($theRule['privilege'] == '') 	$privilege = null;
            else{
                $privilege = explode(":",$theRule['privilege']);
            }
            $resource_arr = array();
            if (!$this->has($theRule['resource_id'])) {
                $this->add( new Zend_Acl_Resource( $theRule['resource_id'] ), $theRule['parent_id'] );
                $resource_arr[]  = $arr[0];
            }
            if ($theRule['controller'] === null) {
                $resource_arr[] = $theRule['resource_id'];
            }
            else {
                $arr = explode(":",$theRule['controller']);
                $resource_arr[] = $theRule['resource_id'].':index'; // 保留進入點權限
                foreach ($arr as $val){
                    if ($val)
                    $resource_arr[] = $theRule['resource_id'].':'.$val;
                }
            }
            //                       echo '<pre>';
            //                        print_r($resource_arr);
            //            echo "----\n";

            foreach($resource_arr as $res_val) {
                if ($this->has($res_val)) {
                    if (is_array($privilege)) {
                        $this->allow( $theRule['role_id'], $res_val, 'index');
                        foreach($privilege as $val) {
                            //	echo "{$theRule['role_id']}, $res_val, $val <BR>";
                            if ($val) {
                                if( $theRule['allow'] == 1 )
                                $this->allow( $theRule['role_id'], $res_val, $val);
                                else {
                                    $this->deny( $theRule['role_id'], $res_val, $val);
                                }
                            }
                        }
                    }
                    else {

                        //    echo "---{$theRule['role_id']}, $res_val, $privilege, {$theRule['allow']} ---<br>";
                        if( $theRule['allow'] == 1 ) {
                            $this->allow( $theRule['role_id'], $res_val,$privilege);
                        }
                        else {
                            //如果有 controller 先加入一筆允許全部項目
                            if ($pos = strpos($res_val,':')) {
                                $this->allow( $theRule['role_id'], substr($res_val,0,$pos).':index',$privilege);
                            }

                            $this->deny( $theRule['role_id'], $res_val,$privilege);
                        }
                    }
                }
            }

        }

        $this->allow(null, 'default');
        $this->deny();
        //$this->allow('Administrator');
    }


    /**
     * 回傳使用者群組
     * @param $role
     * @return unknown_type
     */

    public  function getUserGroup($role)
    {
        $conn = Doctrine_Manager::getInstance()->connection();
        $query = "SELECT DISTINCT r.id FROM acl_roles r LEFT JOIN groups  g ON r.id=g.role_id
        WHERE g.username='$role'";
        $groupsRes = $conn->Execute($query)->fetchAll();

        $arr = array();
        foreach($groupsRes as $groups) {
            $arr[] = $groups['id'];
        }

        if (count($arr)>0)
        return $arr;
        else
        return array('Guest');
    }

    public function userIsAllowed($username,$resource,$controller='',$privilege='')
    {
        // username 擁有的群組
        $roles = $this->getUserGroup($username);
        //print_r($roles);
        //echo $this->isAllowed('Teacher','score') ?'yes':'no';
        $flag = false;
        foreach ($roles as $role){
            if ($this->has($resource) and $this->isAllowed($role,$resource))
            $flag =  true;
            else
            $flag = false;

            foreach (explode(":",$controller) as $cc){
                foreach(explode(":",$privilege) as $pp){
                    if ($pp=='') $pp = NULL;
                    if ($cc=='') {
                        if ($this->has($resource)) {
                            if ($this->isAllowed($role,$resource,$pp))
                            return true;
                        }
                    }else {
                        $rr = $resource.':'.$cc;
                        if ($this->has($rr)) {
                            if ($this->isAllowed($role,$rr,$pp))
                            return true;
                        }
                    }
                }
            }
        }
        return $flag;
    }

}

